////////////////////////////////////////////////////////////////////////////////////////////////
// 
//
// Author: Miajun Yao
// Date: 11/25/2012
// 
////////////////////////////////////////////////////////////////////////////////////////////////



#include "glm/glm.hpp"
#include "glm/gtc/matrix_transform.hpp"

#include <stdio.h>
#include <math.h>
#include <string.h>

#include "lodepng.h"
#include "shaderSetup.h"
#include "GL/glew.h"
#include "GL/glut.h"
#include "GL/gl.h" 

#define M_PI 3.14159


//	define a vertex
typedef struct
{
  float location[3]; 
  float normal[3]; 
  float tangent[3];
  float tex[2];
} Vertex; 


//	define a geometry shape
typedef struct
{
	int nindices;			//	number of indices
	GLuint vboHandle;		//	VBO that contain interleaved positions and colors 
	GLuint indexVBO;		//	index of the VBO
	//	no need to record the vertices and indices after they are passed to GPU VBO
} Geometry;

Geometry cylinder;			//	use cylinder as an example

GLuint programObject;			//	shader program

GLuint colorMap, normalMap;		

GLint c0;						//	position  location
GLint c1;						//	normal location
GLint c2;						//	tangent location
GLint c3;						//	tex coordinate

GLint m0;						//	light position
GLint m1;						//	modelViewMatrix location
GLint m2;						//	normalMatrix location
GLint m3;						//	projectionMatrix location
GLint m4;						//	colorMap location
GLint m5;						//	normalMap location
GLint m6;						//	ambientParameter location
GLint m7;						//	diffuseParameter location
GLint m8;						//	specularParameter location
GLint m9;						//	shinessParameter location


glm::mat4 modelviewMatrix;		//	the input modelView Matrix
glm::mat4 normalMatrix;			//	the input modelView Matrix
glm::mat4 projectionMatrix;		//	the input projection Matrix

/////////////////////////////////
// glut mouse control 
// 
int xform_mode = 0; 
#define XFORM_NONE    0 
#define XFORM_ROTATE  1
#define XFORM_SCALE 2 

int press_x, press_y; 
int release_x, release_y; 
float x_angle = 0.0; 
float y_angle = 0.0; 
float scale_size = 1; 


//	return the length of a vector
float lengthVector(float* vector, int dimension)
{
	float length = 0;
	for (int i = 0; i < dimension; i++)
		length += vector[i] * vector[i];

	return sqrt(length);
}


GLuint createTexture(char* fileName)
{
	glEnable(GL_TEXTURE_2D);
	std::vector<unsigned char> image;
	unsigned width, height;

	unsigned error = lodepng::decode(image, width, height, fileName);
	if(error != 0)
		printf("Error reading png files!\n");

	GLuint tex;
	glGenTextures(1, &tex);
	glBindTexture(GL_TEXTURE_2D, tex);	
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); 
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
	glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, &image[0]);
	glBindTexture(GL_TEXTURE_2D, 0);

	return tex;
}

//	create a Cylinder mesh and create a VBO based on that
void createCylinder(float baseRadius, float topRadius, float height, int slices, int stacks, float r, float g, float b) 
{
	int nvertices = slices * stacks;					//	number of sample vertices
	Vertex* vertex = new Vertex[nvertices];					

	float angleUnit = 2 * M_PI/(float)(slices - 1);		//	angle step between two slices

	//	set up every vertex
	for (int j = 0; j < stacks; j++)
		for (int i = 0; i < slices; i++) 
		{
			float angle = angleUnit * i; 
			float fullBaseX = cos(angle) * baseRadius;
			float fullBaseY = sin(angle) * baseRadius;
			float fullTopX = cos(angle) * topRadius;
			float fullTopY = sin(angle) * topRadius;

			float fullZ = height;

			float rate = j / (float)stacks;
			float pos[] = {fullTopX * rate + fullBaseX * (1 - rate), fullTopY * rate + fullBaseY * (1 - rate), fullZ * rate};
			
			float deltX = fullBaseX - fullTopX;
			float deltY = fullBaseY - fullTopY;
			float deltZ = -fullZ;
			float vector[] = {deltX, deltY, deltZ};			
			float length = lengthVector(vector, 3);
			float lengthRate = deltZ * deltZ / length;
			float normal[] = {fullTopX + lengthRate * deltX, fullTopY + lengthRate * deltY, fullZ + lengthRate * deltZ};
			float tangent[] = {-normal[1], normal[0], 0};
			//	GL_NORMALIZE is enabled so no need to nomalize the normal vector
			
			int count = j * slices + i;
			vertex[count].location[0] = pos[0]; 
			vertex[count].location[1] = pos[1];
			vertex[count].location[2] = pos[2];
			
			vertex[count].normal[0] = normal[0]; 
			vertex[count].normal[1] = normal[1]; 
			vertex[count].normal[2] = normal[2]; 

			vertex[count].tangent[0] = tangent[0]; 
			vertex[count].tangent[1] = tangent[1]; 
			vertex[count].tangent[2] = tangent[2]; 

			vertex[count].tex[0] = (i / (float)slices);
			vertex[count].tex[1] = (j / (float)stacks);
		}

	// set up the index array 
	cylinder.nindices = (stacks-1) * 2 * (slices+1); 
	GLuint *indices = new GLuint[cylinder.nindices]; 
	int n = 0; 
	for (int j = 0; j < stacks-1; j++)
		for (int i = 0; i <= slices; i++) 
		{
			int mi = i % slices;  
			int idx = j * slices + mi;						// mesh[j][mi] 
			int idx2 = (j+1) * slices + mi; 
			indices[n++] = idx; 
			indices[n++] = idx2; 
		}


	//	create VBO based on the vertices and indices
	glGenBuffers(1, &cylinder.vboHandle);						// create an interleaved VBO object
	glBindBuffer(GL_ARRAY_BUFFER, cylinder.vboHandle);		// bind the first handle 
	glBufferData(GL_ARRAY_BUFFER, sizeof(Vertex) * nvertices, vertex, GL_STATIC_DRAW); // allocate space and copy the position data over
	glBindBuffer(GL_ARRAY_BUFFER, 0);   // clean up 

	glGenBuffers(1, &cylinder.indexVBO); 
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, cylinder.indexVBO); 
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLuint) * cylinder.nindices, indices, GL_STATIC_DRAW);  // load the index data 

	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,0);				// clean up 

	delete[] vertex;
	delete[] indices;
}



//	bind the cylinder mesh buffer and draw a cylinder
void drawCylinder()
{
	glBindBuffer(GL_ARRAY_BUFFER, cylinder.vboHandle); 
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, cylinder.indexVBO); 
	glVertexAttribPointer(c0,3,GL_FLOAT, GL_FALSE, sizeof(Vertex),(void*)offsetof(Vertex, location[0])); 
	glVertexAttribPointer(c1,3,GL_FLOAT, GL_FALSE, sizeof(Vertex),(void*)offsetof(Vertex, normal[0])); 
	glVertexAttribPointer(c2,3,GL_FLOAT, GL_FALSE, sizeof(Vertex),(void*)offsetof(Vertex, tangent[0])); 
	glVertexAttribPointer(c3,2,GL_FLOAT, GL_FALSE, sizeof(Vertex),(void*)offsetof(Vertex, tex[0])); 
	glDrawElements(GL_TRIANGLE_STRIP, cylinder.nindices, GL_UNSIGNED_INT, (char*) NULL+0); 
}

void display() 
{ 
	glEnable(GL_DEPTH_TEST);    //	need depth test to correctly draw 3D objects 
	glClearColor(0,0,0,1);		
	glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT); 


	//	set up projection parameters
	projectionMatrix = glm::perspective(60.0f,1.0f,.1f,100.0f); 
	glUniformMatrix4fv(m3, 1, GL_FALSE, &projectionMatrix[0][0]); 

	//	set up view parameters
	glm::mat4 view = glm::lookAt(glm::vec3(0.0, 5.0, 10.0), 
			       glm::vec3(0.0, 0.0, 0.0), 
			       glm::vec3(0.0, 1.0, 0.0)); 

	//	glut transformation parameters
	glm::mat4 model = glm::mat4(1.0f); 
	model = glm::rotate(model, x_angle, glm::vec3(0.0f, 1.0f, 0.0f)); 
	model = glm::rotate(model, y_angle, glm::vec3(1.0f, 0.0f, 0.0f)); 
	model = glm::scale(model,  glm::vec3(scale_size, scale_size, scale_size)); 

	glm::vec4 lightPos = glm::vec4(0.0f, 0.0f, 10.0f, 1.0f);
	lightPos = view * lightPos;
	glUniform3fv(m0, 1, &lightPos[0]);

	// construct the modelview and modelview projection matrices 
	modelviewMatrix = view * model;
	modelviewMatrix = glm::rotate(glm::scale(modelviewMatrix, glm::vec3(5.0, 5.0, 5.0)), -90.0f, glm::vec3(1.0f, 0.0f, 0.0f));
	normalMatrix = glm::transpose(glm::inverse(modelviewMatrix));
	glUniformMatrix4fv(m1, 1, GL_FALSE, &modelviewMatrix[0][0]);
	glUniformMatrix4fv(m2, 1, GL_FALSE, &normalMatrix[0][0]);

	glm::vec3 tmp = glm::vec3(0.1f, 0.1f, 0.1f);			//	ambient light
	glUniform3fv(m6, 1, &tmp[0]);
	tmp = glm::vec3(0.3f, 0.3f, 0.3f);						//	diffuse light parameter
	glUniform3fv(m7, 1, &tmp[0]);
	tmp = glm::vec3(1.0f, 1.0f, 1.0f);						//	specular light
	glUniform3fv(m8, 1, &tmp[0]);
	glUniform1f(m9, 10.0);

	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, colorMap);
	glUniform1i(m4, 0);

	glActiveTexture(GL_TEXTURE1);
	glBindTexture(GL_TEXTURE_2D, normalMap);
	glUniform1i(m5, 1);

	//	draw what you want		
	drawCylinder();

	glutSwapBuffers(); 
} 

//////////////////////////////////////////////////////////////////////////////////
//
//    GLUT stuff 
//
void mymouse(int button, int state, int x, int y)
{
	if (state == GLUT_DOWN) 
	{
		press_x = x; press_y = y; 
		if (button == GLUT_LEFT_BUTTON)
			xform_mode = XFORM_ROTATE; 
		else if (button == GLUT_RIGHT_BUTTON) 
			xform_mode = XFORM_SCALE;   
	}
	else if (state == GLUT_UP) 
	{
		xform_mode = XFORM_NONE; 
	}
}
void mymotion(int x, int y)
{
	if (xform_mode==XFORM_ROTATE) 
	{
		x_angle += (x - press_x)/5.0; 
		if (x_angle > 180) x_angle -= 360; 
		else if (x_angle <-180) x_angle += 360; 
		press_x = x; 
	   
		y_angle += (y - press_y)/5.0; 
		if (y_angle > 180) y_angle -= 360; 
		else if (y_angle <-180) y_angle += 360; 
		press_y = y; 
     }
	else if (xform_mode == XFORM_SCALE)
	{
		float old_size = scale_size;
		scale_size *= (1+ (y - press_y)/60.0); 
		if (scale_size <0) scale_size = old_size; 
		press_y = y; 
    }
    glutPostRedisplay(); 
}


///////////////////////////////////////////////////////////////////////////////
//

int main(int argc, char** argv) 
{ 
	glutInit(&argc, argv); 
	glutInitDisplayMode(GLUT_RGB|GLUT_DOUBLE|GLUT_DEPTH); 
	glutInitWindowSize(600,600); 

	glutCreateWindow("bumpMapping"); 
	glutDisplayFunc(display); 
	glutMouseFunc(mymouse); 
	glutMotionFunc(mymotion);

	//	initialize GLEW 
	GLenum err = glewInit(); 
	if ( err != GLEW_OK)  
	{
		printf("Error initializing GLEW! \n"); 
		exit(1);
	}

	programObject = SetupGLSL("bumpMapping");  //create shaders
	glUseProgram(programObject);

	GLint activeAttris = 0;
	glGetProgramiv(programObject, GL_ACTIVE_ATTRIBUTES, &activeAttris);
	printf("%d\n", activeAttris);

	c0 = glGetAttribLocation(programObject, "aPosition"); 
	c1 = glGetAttribLocation(programObject, "aNormal"); 
	c2 = glGetAttribLocation(programObject, "aTangent"); 
	c3 = glGetAttribLocation(programObject, "aTex"); 
	m0 = glGetUniformLocation(programObject, "uLightPos"); 
	m1 = glGetUniformLocation(programObject, "uModelViewMatrix");
	m2 = glGetUniformLocation(programObject, "uNormalMatrix");
	m3 = glGetUniformLocation(programObject, "uProjectionMatrix");	
	m4 = glGetUniformLocation(programObject, "colorMap");	
	m5 = glGetUniformLocation(programObject, "normalMap");
	m6 = glGetUniformLocation(programObject, "ambientParameter");			
	m7 = glGetUniformLocation(programObject, "diffuseParameter");	
	m8 = glGetUniformLocation(programObject, "specularParameter");	
	m9 = glGetUniformLocation(programObject, "shinessParameter");	

	glEnableVertexAttribArray(c0); 
	glEnableVertexAttribArray(c1); 
	glEnableVertexAttribArray(c2);
	glEnableVertexAttribArray(c3);


	//	create geometry VBOs
	createCylinder(0.5, 0.2, 1.0, 50, 50, 0.5, 0.5, 0.5);
	colorMap = createTexture("color_map.png");
	normalMap = createTexture("normal_map.png");

	glutMainLoop(); 
} 
